/*
 * Copyright 2011 SWM Services GmbH.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package de.swm.commons.mobile.client.widgets.map.impl;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.gwtopenmaps.openlayers.client.Icon;
import org.gwtopenmaps.openlayers.client.LonLat;
import org.gwtopenmaps.openlayers.client.Map;
import org.gwtopenmaps.openlayers.client.MapOptions;
import org.gwtopenmaps.openlayers.client.MapWidget;
import org.gwtopenmaps.openlayers.client.Marker;
import org.gwtopenmaps.openlayers.client.Pixel;
import org.gwtopenmaps.openlayers.client.Size;
import org.gwtopenmaps.openlayers.client.event.MapMoveEndListener;
import org.gwtopenmaps.openlayers.client.event.MapZoomListener;
import org.gwtopenmaps.openlayers.client.event.MarkerBrowserEventListener;
import org.gwtopenmaps.openlayers.client.feature.VectorFeature;
import org.gwtopenmaps.openlayers.client.geometry.LineString;
import org.gwtopenmaps.openlayers.client.geometry.Point;
import org.gwtopenmaps.openlayers.client.layer.Markers;
import org.gwtopenmaps.openlayers.client.layer.OSM;
import org.gwtopenmaps.openlayers.client.layer.Vector;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.TouchEndEvent;
import com.google.gwt.event.dom.client.TouchEndHandler;
import com.google.gwt.event.dom.client.TouchMoveEvent;
import com.google.gwt.event.dom.client.TouchMoveHandler;
import com.google.gwt.event.dom.client.TouchStartEvent;
import com.google.gwt.event.dom.client.TouchStartHandler;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.google.inject.Inject;

import de.swm.commons.mobile.client.SWMMobile;
import de.swm.commons.mobile.client.utils.Utils;
import de.swm.commons.mobile.client.widgets.map.IMapController;
import de.swm.commons.mobile.client.widgets.map.IMapView;
import de.swm.commons.mobile.client.widgets.map.MapConfiguration;
import de.swm.commons.mobile.client.widgets.map.data.IStationData;
import de.swm.commons.mobile.client.widgets.map.data.LatLng;
import de.swm.commons.mobile.client.widgets.map.handlers.IMapMoveHandler;
import de.swm.commons.mobile.client.widgets.map.handlers.IMapZoomHandler;
import de.swm.commons.mobile.client.widgets.map.handlers.IStationPickHandler;

/**
 * Generelles Panel um eine Karte darzustellen.
 * 
 * @author vilbig.alexander
 * @author wimmel.guido
 * @author wiese.daniel <br>
 *         copyright (C) 2011, SWM Services GmbH
 * 
 */
public class MapPanel implements IMapView {

	private static final String EPSG_4326_PROJECTION = "EPSG:4326";
	private static final int PADDING = 4;
	private static final Pixel PIXEL_OFFSET = new Pixel(-14, -48);
	private static final double STROKE_WIDTH = 4.0;
	private static final int NUM_ZOOM_LEVELS = 16;
	/** Open map ID. **/
	private static final String OPEN_LAYER_MAP_WIDGET_ID = "ol-map-widget";
	private static final String MARKERS = "Markers";
	private static final String ROUTING = "Routing";
	private static final String CONNECTION_PART = "ConnectionPart";

	private final ImageResource stationMarkerImage;
	private final ImageResource ownPositonMarkerImage;

	private MapWidget mapWidget;
	private Map map;
	private ZoomControlPanel zoomControl;
	private Markers markerLayer;
	private Markers routingLayer;
	private Vector connectionPartLayer;
	private Marker positionMarker;
	private Set<IStationData> displayedStations;
	private java.util.Map<IStationData, Marker> stationMarkers;
	private MarkerPopup ownPositionPopup;
	private java.util.Map<IStationData, MarkerPopup> stationPopups;
	private IStationPickHandler originStationPickHandler;
	private IStationPickHandler targetStationPickHandler;
	private IMapMoveHandler mapMoveHandler;
	private IMapZoomHandler mapZoomHandler;
	private IStationData originStation;
	private IStationData targetStation;
	private Marker originMarker;
	private Marker targetMarker;
	private final IMapController controller;
	private OSM osmLayer;

	/**
	 * Default constructor.
	 * 
	 * @param controller
	 *            .
	 */
	@Inject
	public MapPanel(IMapController controller) {
		this.controller = controller;
		this.stationMarkerImage = SWMMobile.getTheme().getMGWTImageBundle()
				.mapStation_pin_normal();
		this.ownPositonMarkerImage = SWMMobile.getTheme().getMGWTImageBundle()
				.mapPin_red();

		MapOptions defaultMapOptions = new MapOptions();
		defaultMapOptions.removeDefaultControls();
		defaultMapOptions.setNumZoomLevels(NUM_ZOOM_LEVELS);
		defaultMapOptions.setProjection(MapPanel.EPSG_4326_PROJECTION);

		mapWidget = new MapWidget("100%", "100%", defaultMapOptions);
		DOM.setElementProperty(mapWidget.getElement(), "id",
				OPEN_LAYER_MAP_WIDGET_ID);
		mapWidget.getElement().getStyle().setPosition(Style.Position.RELATIVE);

		map = mapWidget.getMap();
		osmLayer = controller.getOSMLayer();
		osmLayer.setIsBaseLayer(true);
		map.addLayer(osmLayer);

		LatLng geopos = MapConfiguration.getDefaultPos();
		GWT.log("Center Map: " + geopos);
		LonLat pos = new LonLat(geopos.getLongitude(), geopos.getLatitude());
		pos.transform(MapPanel.EPSG_4326_PROJECTION, map.getProjection());
		map.setCenter(pos, MapConfiguration.getMapInitialZoom());

		map.addControl(new TouchNavigation());

		displayedStations = new HashSet<IStationData>();
		stationMarkers = new HashMap<IStationData, Marker>();
		stationPopups = new HashMap<IStationData, MarkerPopup>();
		connectionPartLayer = new Vector(CONNECTION_PART);
		map.addLayer(connectionPartLayer);
		markerLayer = new Markers(MARKERS);
		map.addLayer(markerLayer);
		routingLayer = new Markers(ROUTING);
		map.addLayer(routingLayer);

		map.addMapZoomListener(new MapZoomListener() {

			@Override
			public void onMapZoom(MapZoomEvent eventObject) {
				if (mapZoomHandler != null) {
					mapZoomHandler.onMapZoomed(map.getZoom());
				}
			}
		});

		map.addMapMoveEndListener(new MapMoveEndListener() {

			@Override
			public void onMapMoveEnd(MapMoveEndEvent eventObject) {
				if (mapMoveHandler != null) {
					LonLat centerPos = map.getCenter();
					centerPos.transform(map.getProjection(),
							MapPanel.EPSG_4326_PROJECTION);
					mapMoveHandler.onMapMoved(LatLng.newInstance(
							centerPos.lat(), centerPos.lon()));
				}
			}
		});

		controller.setMapView(this);
	}

	/**
	 * Aedert den aktuellen Layer.
	 */
	public void changeLayer() {
		map.removeLayer(osmLayer);
		OSM osmLayer = controller.getOSMLayer();
		this.osmLayer.setIsBaseLayer(true);
		this.osmLayer = osmLayer;
		map.addLayer(this.osmLayer);
	}

	@Override
	public Widget asWidget() {
		if (zoomControl == null) {
			zoomControl = new ZoomControlPanel(map);
			zoomControl.init();
		}
		return mapWidget;
	}

	@Override
	public void updateStationMarkers(List<? extends IStationData> stations) {
		Utils.console("updateStations called...");

		Set<IStationData> stationNames = new HashSet<IStationData>();
		int newStations = 0;
		for (IStationData data : stations) {
			if (data.isOrigin() || data.isTarget()) {
				if (data.isOrigin() && !data.equals(originStation)) {
					if (originMarker != null) {
						routingLayer.removeMarker(originMarker);
					}
					originStation = data;
					originMarker = createStationMarker(data);
					routingLayer.addMarker(originMarker);
				}
				if (data.isTarget() && !data.equals(targetStation)) {
					if (targetMarker != null) {
						routingLayer.removeMarker(targetMarker);
					}
					targetStation = data;
					targetMarker = createStationMarker(data);
					routingLayer.addMarker(targetMarker);
				}
			} else {
				stationNames.add(data);
				if (!displayedStations.contains(data)) {
					Marker sm = createStationMarker(data);
					markerLayer.addMarker(sm);
					displayedStations.add(data);
					stationMarkers.put(data, sm);
					newStations++;
				}
			}
		}

		int oldStations = 0;
		for (IStationData name : displayedStations) {
			if (!stationNames.contains(name)) {
				Marker m = stationMarkers.get(name);
				markerLayer.removeMarker(m);
				MarkerPopup popup = stationPopups.get(name);
				if (popup != null) {
					popup.destroy();
					stationPopups.remove(name);
				}
				oldStations++;
			}
		}
		displayedStations.retainAll(stationNames);

		Utils.console("updateStations created " + newStations
				+ " new markers and removed " + oldStations
				+ " old markers, currently showing " + displayedStations.size()
				+ " stations");
	}

	@Override
	public void drawWaypoints(LonLat currentPos, LonLat stationPos,
			final List<Marker> stationMarkers) {
		// TODO: implement function
	}

	@Override
	public void displayConnectionRoute(List<? extends LatLng> gpsPoints) {

		connectionPartLayer.destroyFeatures();
		GWT.log("Pfadlänge: " + gpsPoints.size());

		Point[] linePoints = new Point[gpsPoints.size()];
		int i = 0;
		for (LatLng point : gpsPoints) {
			LonLat pos = new LonLat(point.getLongitude(), point.getLatitude());
			pos.transform(MapPanel.EPSG_4326_PROJECTION, map.getProjection());
			linePoints[i++] = new Point(pos.lon(), pos.lat());
		}

		if (linePoints.length > 1) {
			LineString line = new LineString(linePoints);
			org.gwtopenmaps.openlayers.client.Style lineStyle = new org.gwtopenmaps.openlayers.client.Style();
			lineStyle.setFillColor("red");
			lineStyle.setStrokeColor("red");
			lineStyle.setStrokeWidth(STROKE_WIDTH);
			VectorFeature lineFeature = new VectorFeature(line, lineStyle);
			connectionPartLayer.addFeature(lineFeature);
			map.panTo(new LonLat(linePoints[0].getX(), linePoints[0].getY()));
		}
	}

	@Override
	public void centerPosition(LatLng pos) {
		GWT.log("Center Map: " + pos);
		LonLat position = new LonLat(pos.getLongitude(), pos.getLatitude());
		position.transform(MapPanel.EPSG_4326_PROJECTION, map.getProjection());
		try {
			map.panTo(position);
		} catch (Exception e) {
			GWT.log("Warnung - Ausnahme bei centerPosition: " + e.getMessage());
			Utils.console("Warnung - Ausnahme bei centerPosition: "
					+ e.getMessage());
		}
	}

	@Override
	public void centerPosition(LatLng pos, int zoom) {
		GWT.log("Center Map: " + pos + " Zoom: " + zoom);
		LonLat position = new LonLat(pos.getLongitude(), pos.getLatitude());
		position.transform(MapPanel.EPSG_4326_PROJECTION, map.getProjection());
		try {
			map.setCenter(position, zoom);
		} catch (Exception e) {
			GWT.log("Warnung - Ausnahme bei centerPosition: " + e.getMessage());
			Utils.console("Warnung - Ausnahme bei centerPosition: "
					+ e.getMessage());
		}
	}

	@Override
	public void centerToDefaultPos() {
		final LatLng geopos = MapConfiguration.getDefaultPos();
		centerPosition(geopos);
	}

	@Override
	public void setCurrentPosition(LatLng pos) {
		LonLat position = new LonLat(pos.getLongitude(), pos.getLatitude());
		position.transform(MapPanel.EPSG_4326_PROJECTION, map.getProjection());
		if (positionMarker != null) {
			routingLayer.removeMarker(positionMarker);
			positionMarker = null;
		}
		if (positionMarker == null) {
			Size size = new Size(ownPositonMarkerImage.getWidth(),
					ownPositonMarkerImage.getHeight());
			Pixel offset = new Pixel(-(size.getWidth() / 2), -size.getHeight());
			Icon positionIcon = new Icon(ownPositonMarkerImage.getSafeUri()
					.asString(), size, offset);
			positionMarker = new Marker(position, positionIcon);
			String event = "touchstart";
			if (!GWT.isScript()) {
				event = "mousedown";
			}
			positionMarker.addBrowserEventListener(event,
					new MarkerBrowserEventListener() {

						public void onBrowserEvent(
								MarkerBrowserEvent markerBrowserEvent) {
							handlePositionMarker(markerBrowserEvent.getSource());
							positionMarker.getEvents().stop(markerBrowserEvent);
						}

					});
			routingLayer.addMarker(positionMarker);
		}
	}

	@Override
	public void setStationVisibility(boolean isVisible) {
		if (markerLayer != null) {
			if (isVisible) {
				if (!markerLayer.isVisible()) {
					markerLayer.setIsVisible(true);
				}
			} else {
				if (markerLayer.isVisible()) {
					markerLayer.setIsVisible(false);
					clearStationPopups();
				}
			}
		}
	}

	/**
	 * Loescht die Stationsmarkierungen.
	 */
	private void clearStationPopups() {
		for (MarkerPopup p : stationPopups.values()) {
			p.destroy();
		}
		stationPopups.clear();
	}

	/**
	 * Erzeugt die Stationsmarkierungen.
	 * 
	 * @param data
	 *            die stationsdaten
	 * @return der marker der Station
	 */
	private Marker createStationMarker(final IStationData data) {
		LonLat position = new LonLat(data.getPos().getLongitude(), data
				.getPos().getLatitude());
		position.transform(MapPanel.EPSG_4326_PROJECTION, map.getProjection());
		Icon stationIcon;
		if (data.isOrigin()) {
			stationIcon = createOriginIcon();
		} else if (data.isTarget()) {
			stationIcon = createTargetIcon();
		} else {
			stationIcon = createNormalIcon();
		}
		Marker result = null;
		try {
			result = new Marker(position, stationIcon);
			String event = "touchstart";
			if (!GWT.isScript()) {
				event = "mousedown";
			}
			result.addBrowserEventListener(event,
					new MarkerBrowserEventListener() {

						public void onBrowserEvent(
								MarkerBrowserEvent markerBrowserEvent) {
							// hier den langnamen verwenden
							handleStationMarker(markerBrowserEvent.getSource(),
									data);
						}

					});
		} catch (Exception e) {
			Utils.console("Warning - exception while creating station marker: "
					+ e.getMessage());
		}
		return result;
	}

	/**
	 * Erzeugt das Icon fuer eine nicht gewaehlte Station.
	 * 
	 * @return das Icon
	 */
	private Icon createNormalIcon() {
		Size size = new Size(stationMarkerImage.getWidth(),
				stationMarkerImage.getHeight());
		return new Icon(stationMarkerImage.getSafeUri().asString(), size,
				PIXEL_OFFSET);
	}

	/**
	 * Erzeugt das Icon fuer eine gewaehlte ziel Station.
	 * 
	 * @return das Icon
	 */
	private Icon createTargetIcon() {
		final ImageResource target = SWMMobile.getTheme().getMGWTImageBundle()
				.mapStation_pin_target();
		Size size = new Size(target.getWidth(), target.getHeight());
		return new Icon(target.getSafeUri().asString(), size, PIXEL_OFFSET);
	}

	/**
	 * Erzeugt das Icon fuer eine gewaehlte abfahrt Station.
	 * 
	 * @return das Icon
	 */
	private Icon createOriginIcon() {
		final ImageResource origin = SWMMobile.getTheme().getMGWTImageBundle()
				.mapStation_pin_origin();
		Size size = new Size(origin.getWidth(), origin.getHeight());
		return new Icon(origin.getSafeUri().asString(), size, PIXEL_OFFSET);
	}

	/**
	 * Zeigt ein popup fuer einen Stationsmarker.
	 * 
	 * @param m
	 *            der marker
	 * @param station
	 *            der name
	 */
	private void handleStationMarker(Marker m, IStationData station) {
		MarkerPopup popup = stationPopups.get(station);
		if (popup == null) {
			popup = createStationPopup(m, station);
			stationPopups.put(station, popup);
		}
		popup.toggleVisibility();
	}

	/**
	 * Zeigt ein popup fuer einen Positionsmarker.
	 * 
	 * @param m
	 *            der marker
	 */
	private void handlePositionMarker(Marker m) {
		if (ownPositionPopup == null) {
			ownPositionPopup = createPositionPopup(m.getLonLat());
		}
		ownPositionPopup.toggleVisibility();
	}

	/**
	 * Erzeugt ein popup fuer einen Positionsmarker.
	 * 
	 * @param pos
	 *            die aktuelle position
	 * @return das popup.
	 */
	private MarkerPopup createPositionPopup(LonLat pos) {
		MarkerPopup result = new MarkerPopup(map, pos, 0,
				-(ownPositonMarkerImage.getHeight() + 2),
				MarkerPopup.Anchor.CENTER_BOTTOM);
		result.getContentArea().add(new Label("Aktuelle Position"));
		return result;
	}

	/**
	 * FIXME: Hier die Karte modifizieren! Erzeugt ein popup fuer einen
	 * Stationsmarker.
	 * 
	 * @param marker
	 *            der marker
	 * @param station
	 *            der name
	 * @return das popup.
	 */
	private MarkerPopup createStationPopup(final Marker marker,
			final IStationData station) {
		final MarkerPopup result = new MarkerPopup(map, marker.getLonLat(), 0,
				-((stationMarkerImage.getHeight()) + 2),
				MarkerPopup.Anchor.CENTER_BOTTOM);
		// origin Station
		if (originStationPickHandler != null) {
			final Image goToStationImage = new Image(SWMMobile.getTheme()
					.getMGWTImageBundle().mapPopoupInnerImageOriginStation());
			goToStationImage.setStyleName(SWMMobile.getTheme()
					.getMGWTCssBundle().getMapCss().markerPopupImage());
			goToStationImage.addClickHandler(new ClickHandler() {

				@Override
				public void onClick(ClickEvent event) {

					if (originStationPickHandler != null) {
						originStationPickHandler.onStationPicked(station);
					}
					if (originStation == null) {
						switchOriginStation(marker, station);
					}
					result.hide();
				}
			});
			goToStationImage.addTouchStartHandler(new TouchStartHandler() {

				public void onTouchStart(TouchStartEvent event) {
					goToStationImage.addStyleName(SWMMobile.getTheme()
							.getMGWTCssBundle().getMapCss().pressed());
				}
			});
			goToStationImage.addTouchEndHandler(new TouchEndHandler() {

				public void onTouchEnd(TouchEndEvent event) {
					goToStationImage.removeStyleName(SWMMobile.getTheme()
							.getMGWTCssBundle().getMapCss().pressed());
					result.hide();
				}
			});
			goToStationImage.addTouchMoveHandler(new TouchMoveHandler() {

				public void onTouchMove(TouchMoveEvent event) {
					goToStationImage.removeStyleName(SWMMobile.getTheme()
							.getMGWTCssBundle().getMapCss().pressed());
					result.hide();
				}
			});
			result.getContentArea().add(goToStationImage);
		}

		Label text = new Label(station.getShortName());
		text.getElement().getStyle().setPaddingLeft(PADDING, Unit.PX);
		text.getElement().getStyle().setPaddingRight(PADDING, Unit.PX);
		result.getContentArea().add(text);

		// target Station
		if (targetStationPickHandler != null) {

			final Image checkeredFlag = new Image(SWMMobile.getTheme()
					.getMGWTImageBundle().mapPopoupInnerImageTargetStation());
			checkeredFlag.setStyleName(SWMMobile.getTheme().getMGWTCssBundle()
					.getMapCss().markerPopupImage());
			checkeredFlag.addClickHandler(new ClickHandler() {

				@Override
				public void onClick(ClickEvent event) {
					if (targetStationPickHandler != null) {
						targetStationPickHandler.onStationPicked(station);
					}
					if (targetStation == null) {
						switchTargetStation(marker, station);
					}
					result.hide();
				}
			});
			checkeredFlag.addTouchStartHandler(new TouchStartHandler() {

				public void onTouchStart(TouchStartEvent event) {
					checkeredFlag.addStyleName(SWMMobile.getTheme()
							.getMGWTCssBundle().getMapCss().pressed());
				}
			});
			checkeredFlag.addTouchEndHandler(new TouchEndHandler() {

				public void onTouchEnd(TouchEndEvent event) {
					checkeredFlag.removeStyleName(SWMMobile.getTheme()
							.getMGWTCssBundle().getMapCss().pressed());
					result.hide();
				}
			});
			checkeredFlag.addTouchMoveHandler(new TouchMoveHandler() {

				public void onTouchMove(TouchMoveEvent event) {
					checkeredFlag.removeStyleName(SWMMobile.getTheme()
							.getMGWTCssBundle().getMapCss().pressed());
					result.hide();
				}
			});
			result.getContentArea().add(checkeredFlag);
		}

		return result;
	}

	@Override
	public void setOriginStation(IStationData station) {
		switchOriginStation(stationMarkers.get(station), station);
	}

	@Override
	public void setTargetStation(IStationData station) {
		switchTargetStation(stationMarkers.get(station), station);
	}

	@Override
	public void setOriginStationPickHandler(IStationPickHandler handler) {
		originStationPickHandler = handler;
	}

	@Override
	public void setTargetStationPickHandler(IStationPickHandler handler) {
		targetStationPickHandler = handler;
	}

	@Override
	public void setMapMoveHandler(IMapMoveHandler handler) {
		mapMoveHandler = handler;
	}

	@Override
	public void setMapZoomHandler(IMapZoomHandler handler) {
		mapZoomHandler = handler;
	}

	/**
	 * add/removal of respective markers, necessary because of weird behavior in
	 * Marker.setIcon() and // Marker.setImageUrl().
	 * 
	 * @param marker
	 *            der marker
	 * @param station
	 *            die station
	 */
	private void switchOriginStation(Marker marker, IStationData station) {
		displayedStations.remove(station);
		stationMarkers.remove(station);
		if (marker != null) {
			markerLayer.removeMarker(marker);
		}
		station.setOrigin(true);
		Marker newOriginMarker = createStationMarker(station);
		routingLayer.addMarker(newOriginMarker);

		// check for previous origin marker and reset to normal
		if (originStation != null && originMarker != null) {
			routingLayer.removeMarker(originMarker);
			originStation.setOrigin(false);
			Marker normal = createStationMarker(originStation);
			markerLayer.addMarker(normal);
			displayedStations.add(station);
			stationMarkers.put(station, normal);
		}

		// set new origin marker
		originStation = station;
		originMarker = newOriginMarker;
	}

	/**
	 * add/removal of respective markers, necessary because of weird behavior in
	 * Marker.setIcon() and // Marker.setImageUrl().
	 * 
	 * @param marker
	 *            der marker
	 * @param station
	 *            die station
	 */
	private void switchTargetStation(Marker marker, IStationData station) {
		displayedStations.remove(station);
		stationMarkers.remove(station);
		if (marker != null) {
			markerLayer.removeMarker(marker);
		}
		station.setTarget(true);
		Marker newTargetMarker = createStationMarker(station);
		routingLayer.addMarker(newTargetMarker);

		// check for previous target marker and reset to normal
		if (targetStation != null && targetMarker != null) {
			routingLayer.removeMarker(targetMarker);
			targetStation.setTarget(false);
			Marker normal = createStationMarker(targetStation);
			markerLayer.addMarker(normal);
			displayedStations.add(station);
			stationMarkers.put(station, normal);
		}

		// set new target marker
		targetStation = station;
		targetMarker = newTargetMarker;
	}

}
